Discovery service (Flow Acquisitions)
This section of the developer guide focuses on handling proof requests and responses within the One37ID SDK, specifically focusing on acquiring and verifying verifiable credentials.
A key element of the flow involves checking whether the user possesses the required credentials. If the necessary verifiable credentials are not found, the application must guide the user to acquire them by initiating a flow acquisition.
Purpose
The goal of this feature is to allow users to seamlessly fulfill proof requests with verifiable credentials, ensuring that they are either acquired through a predefined flow or provided through existing credentials.
Key Concepts
- Proof Requests: These are requests sent by the application or a third party to verify specific information about the user, like a verified email address or driver’s license.
- Proof Responses: These are responses from the user containing the requested verifiable credentials or self-attested information.
- Verifiable Credentials: These are credentials issued by trusted entities and can be cryptographically verified.
- Flows: When the required verifiable credential is not found, the application can initiate an acquisition flow, which guides the user to obtain the missing credential.
Acquiring New Credentials
If a proof request involves verifiable credentials and those credentials are not found in the user’s profile, the application needs to initiate a flow to acquire them. This section describes how to perform this action using the Flow Selection Modal and how the process interacts with the One37ID SDK.
Code Explanation and Process
-
Checking for Available Credentials
else if (selectedItem.type === RequestedDataValueType.VerifiableCredential) {
try {
if (selectedItem.availableCredentials.length > 0) {
setResult(prevResult => [
...prevResult,
{ id: selectedItem.id, selected: selectedItem.availableCredentials[0].id },
]);
updatedData = updatedData.map(reqItem =>
reqItem.id === selectedItem.id ? { ...reqItem, isAcquired: true } : reqItem
);
setRequestData(updatedData);
} else {
const acquiredFlows = await agentInstance.presentationRequestManager.acquireFlows(selectedItem);
if (acquiredFlows.length > 0) {
setFlows(acquiredFlows);
setModalVisible(true);
} else {
alert('There are no available flows to obtain the credential.');
}
}
} catch (error) {
console.error('Error acquiring verifiable credential:', error);
}
}
```Example selectedItem Object:
{
"attributes": [
{
"isSystem": true,
"name": "_namespace"
},
{
"isSystem": false,
"name": "givennames"
},
{
"isSystem": false,
"name": "identifier"
},
{
"isSystem": false,
"name": "dateofbirth"
},
{
"isSystem": false,
"name": "sex"
},
{
"isSystem": false,
"name": "countryofissuance"
},
{
"isSystem": false,
"name": "issuingstatecode"
}
],
"availableCredentials": [],
"id": "0192d729-ef56-7cce-a50a-3759a5ae3658-drivers-license",
"isAcquired": false,
"isMultipleCandidates": false,
"metadata": {
"inputDescriptor": "eyJpZCI6IjAxOTJkNzI5LWVmNTYtN2NjZS1hNTBhLTM3NTlhNWFlMzY1OC1kcml2ZXJzLWxpY2Vuc2UiLCJuYW1lIjoiRHJpdmVycyBMaWNlbnNlIiwicHVycG9zZSI6IkRyaXZlcnMgTGljZW5zZSIsInNjaGVtYSI6W3sidXJpIjoiaHR0cHM6Ly8xMzcuZGV2LW9uZTM3LmlkL2JjL3B1YmxpYy9zY2hlbWFzL2NvbS5vbmUzN2lkLmlkZW50aXR5Y2FyZC8xLjAifV0sImNvbnN0cmFpbnRzIjp7ImZpZWxkcyI6W3sicGF0aCI6WyIkLmNyZWRlbnRpYWxTdWJqZWN0Ll9uYW1lc3BhY2UiLCIkLnZjLmNyZWRlbnRpYWxTdWJqZWN0Ll9uYW1lc3BhY2UiXSwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJjb25zdCI6InBlcnNvbmFsLmJpb2dyYXBoaWMuaWRlbnRpdHljYXJkIn19LHsicGF0aCI6WyIkLmNyZWRlbnRpYWxTdWJqZWN0LmdpdmVubmFtZXMiLCIkLnZjLmNyZWRlbnRpYWxTdWJqZWN0LmdpdmVubmFtZXMiXX0seyJwYXRoIjpbIiQuY3JlZGVudGlhbFN1YmplY3QuaWRlbnRpZmllciIsIiQudmMuY3JlZGVudGlhbFN1YmplY3QuaWRlbnRpZmllciJdfSx7InBhdGgiOlsiJC5jcmVkZW50aWFsU3ViamVjdC5kYXRlb2ZiaXJ0aCIsIiQudmMuY3JlZGVudGlhbFN1YmplY3QuZGF0ZW9mYmlydGgiXX0seyJwYXRoIjpbIiQuY3JlZGVudGlhbFN1YmplY3Quc2V4IiwiJC52Yy5jcmVkZW50aWFsU3ViamVjdC5zZXgiXX0seyJwYXRoIjpbIiQuY3JlZGVudGlhbFN1YmplY3QuY291bnRyeW9maXNzdWFuY2UiLCIkLnZjLmNyZWRlbnRpYWxTdWJqZWN0LmNvdW50cnlvZmlzc3VhbmNlIl19LHsicGF0aCI6WyIkLmNyZWRlbnRpYWxTdWJqZWN0Lmlzc3VpbmdzdGF0ZWNvZGUiLCIkLnZjLmNyZWRlbnRpYWxTdWJqZWN0Lmlzc3VpbmdzdGF0ZWNvZGUiXX1dfX0="
},
"name": "Drivers License",
"purpose": "Drivers License",
"restrictions": [
{
"condition": "equals",
"name": "schemaId",
"value": "https://137.dev-one37.id/bc/public/schemas/com.one37id.identitycard/1.0"
},
{
"condition": "equals",
"name": "_namespace",
"value": "personal.biographic.identitycard"
}
],
"type": "vc"
}Explanation:
- The code first checks if the selectedItem (an item requested in the proof request) is a Verifiable Credential.
- If the user already possesses the required credential, the first available credential is selected and marked as acquired.
- If the required credential is not found, the application attempts to acquire a flow using
acquireFlows(selectedItem)
. This returns a list of flows that can provide the missing credential. If no flows are found, an alert is shown to the user.
-
Example Acquired Flow Object
[
{
"contact": {
"did": "did:web:137.dev-one37.id",
"displayName": "One37 Solutions, Inc.",
"id": "44bb4ffc-75ec-4549-aa2a-0edb44da5873",
"isConnected": true,
"logoUrl": "https://cdn.one37id.com/static/images/one37.png"
},
"namespace": "com.one37id.validate.driverslicense",
"version": 1
}
]Explanation: This JSON represents an acquired flow object, which includes the contact details of the issuing entity and information about the flow, such as its namespace and version.
-
Selecting and Starting a Flow
//Example modal
<FlowSelectionModal
visible={modalVisible}
flows={flows}
onSelectFlow={async flow => {
setIsFlowProcessing(true);
const agent = await initializeAgent();
if (!agent) {
console.error('Agent instance not found');
return;
}
try {
const selectedItem = requestData.find(item => item.id === item.id) as PresentationRequestItem;
if (flow.contact.id) {
await agent.contactManager.connectWS({ id: flow.contact.id });
}
const startFlowResult = await agent.presentationRequestManager.startAvailableFlow(selectedItem, flow);
if (!startFlowResult.isSuccessful) {
console.error(`Failed to start flow. Error: ${startFlowResult.error}`);
alert(`Failed to start flow: ${startFlowResult.error}`);
}
} catch (error) {
console.error('Error starting flow:', error);
alert('An error occurred while starting the flow.');
} finally {
setIsFlowProcessing(false);
setModalVisible(false);
setFlows([]);
}
}}
onCancel={() => {
setModalVisible(false);
setFlows([]);
}}
isProcessing={isFlowProcessing}
/>Example flow object:
{
"contact": {
"did": "did:web:137.dev-one37.id",
"displayName": "One37 Solutions, Inc.",
"id": "44bb4ffc-75ec-4549-aa2a-0edb44da5873",
"isConnected": true,
"logoUrl": "https://cdn.one37id.com/static/images/one37.png"
},
"namespace": "com.one37id.validate.driverslicense",
"version": 1
}Explanation:
- This code snippet handles the selection of a flow from the Flow Selection Modal.
- Upon selecting a flow, the app connects to the contact’s websocket (
connectWS
) to establish communication. - Then, it starts the selected flow using
startAvailableFlow()
, passing in the selected item and the chosen flow.
-
Triggering the Presentation Request Callback
const startFlowResult =
await agent.presentationRequestManager.startAvailableFlow(
selectedItem,
flow
);Explanation:
- This function triggers the flow acquisition using the selected flow. It establishes a connection and requests the missing credentials, leading to the presentation of the flow screen where the user must provide the required information.
- This function will also trigger the presentationRequestCallback in the callbackHandlers, allowing the app to proceed with handling the specific proof request. After this, the user is navigated to the Proof Request / Response screen to fill in any missing data for the required credentials.
For more details on configuring the callback, refer to the Quickstart Guide's presentationRequestCallback section.
Handling Credential Offers and Automatic Acquisition in Proof Request / Response
When managing credential offers in the Proof Request and Response screen, the workflow includes receiving, verifying, and accepting credential offers, followed by updating the request state automatically when a new credential is issued.
1. Receiving and Displaying Credential Offers
When the necessary credentials are successfully acquired and verified, the credentialOfferCallback
in the callbackHandlers
is triggered. This callback function, OnCredentialOffer
, receives a CredentialOfferCallbackModel
object and presents the user with a choice to accept or reject the offered credential.
The Callback Function:
async function OnCredentialOffer(
credentialOffer: CredentialOfferCallbackModel
): Promise<CredentialOfferCallbackResponse> {
console.log("---CredentialOffered:", credentialOffer);
let credentialOfferCallbackResponse: CredentialOfferCallbackResponse;
credentialOffer.credentials.forEach((vc) => {
console.log("---CredentialOffered type:", vc.type);
});
return new Promise((resolve) => {
RootNavigation.navigate(ScreenRoutesEnum.CredentialPopupModal, {
title: "Credential offer",
details: getCredentialLabelsString(credentialOffer.credentials),
extraDetails: getCredentialAttributesArray(credentialOffer.credentials),
primaryButton: {
caption: "Accept",
onPress: async () => {
credentialOfferCallbackResponse = {
action: CredentialOfferCallbackAction.Accept,
};
resolve(credentialOfferCallbackResponse);
await delay(500);
RootNavigation.goBack();
},
},
secondaryButton: {
caption: "Reject",
onPress: async () => {
credentialOfferCallbackResponse = {
action: CredentialOfferCallbackAction.Reject,
};
resolve(credentialOfferCallbackResponse);
RootNavigation.goBack();
},
},
onClose: async () => {
credentialOfferCallbackResponse = {
action: CredentialOfferCallbackAction.Later,
};
resolve(credentialOfferCallbackResponse);
RootNavigation.goBack();
},
});
});
}
Explanation: The function navigates to a screen displaying the credential offer. Users can view the details and either accept or reject it. The acceptance triggers the acquisition process.
2. Handling Credential Acceptance and Triggering an Event
When a user accepts a credential offer, it triggers the onCredentialAccepted
event in the eventHandlers
object.
Event Handler:
onCredentialAccepted: async (credentials: Credential[]) => {
console.log("---------------------onCredentialAccepted:", credentials);
credentialEventEmitter.emit("credentialIssued", {
credentials: credentials,
});
};
Explanation: This function triggers an event named credentialIssued
, carrying the accepted credentials to automatically update the list of required credentials.
3. Listening for Credential Issuance in Proof Request / Response
In the Proof Request / Response screen, the listener for the credentialIssued
event is set up within a useEffect
hook. This listener updates the state of request items with newly accepted credentials.
State Update in useEffect
:
useEffect(() => {
const initAgent = async () => {
const updatedRequestData = presentationRequest.items.map((item) => {
const isAvailable = item.availableCredentials.length > 0;
const isMultipleCandidates = item.availableCredentials.length > 1;
if (isAvailable) {
setResult((prevResult) => [
...prevResult,
{ id: item.id, selected: item.availableCredentials[0].id },
]);
}
return {
...item,
isAcquired: isAvailable,
isMultipleCandidates: isMultipleCandidates,
};
});
setRequestData(updatedRequestData);
};
const handleCredentialIssued = async (data: {
credentials: Credential[],
}) => {
const newCandidates = await getPresentationRequestCandidates(
data.credentials,
presentationRequest
);
const updatedItems = presentationRequest.items.map((item) => {
const isAvailable = item.availableCredentials.length > 0;
const isMultipleCandidates = item.availableCredentials.length > 1;
if (isAvailable) {
setResult((prevResult) => [
...prevResult,
{ id: item.id, selected: item.availableCredentials[0].id },
]);
}
return {
...item,
isAcquired: isAvailable,
isMultipleCandidates: isMultipleCandidates,
};
});
setRequestData(updatedItems);
};
initAgent();
credentialEventEmitter.on("credentialIssued", handleCredentialIssued);
return () => {
credentialEventEmitter.off("credentialIssued", handleCredentialIssued);
};
}, [presentationRequest]);
Explanation: Upon receiving the event, it updates the request state by marking items as acquired if the newly issued credentials match the requirements.
4. Helper Function: getPresentationRequestCandidates
**
The getPresentationRequestCandidates
function is used to match new credentials with the items in the Proof Request. This function compares the newly issued credentials against the items in the current proof request to identify any matches.
Function Definition:
const getPresentationRequestCandidates = async (
credentials: Credential[],
proof: PresentationRequest
): Promise<PresentationRequestItemCandidates[]> => {
agent = await initializeAgent();
if (!agent) {
throw new Error("Agent not initialized");
}
console.log(
"getPresentationRequestCandidates *** credentials:",
credentials,
"proof: ",
proof
);
const result =
await agent.presentationRequestManager.checkPresentationRequestCandidates(
credentials,
proof
);
console.log("getPresentationRequestCandidates result:", result);
if (!result.isSuccessful) {
console.error(`[BASIC MESSAGE] Error: ${result.error}`);
return [];
} else {
if (result.result) {
console.log("getPresentationRequestCandidates:", result.result);
return result.result;
}
return [];
}
};
-
Parameters:
credentials
: An array ofCredential
objects that were issued and need to be checked against the proof request.proof
: The currentPresentationRequest
containing the items that need credentials.
-
Returns: A promise that resolves to an array of
PresentationRequestItemCandidates[]
containing matched items for which the issued credentials are applicable. -
Purpose: This function checks which issued credentials can be matched to the items in the proof request. If a match is found, the state is updated accordingly to reflect that the item is acquired.
Important Note
In this scenario, we used event emitters to handle updates when specific actions, like credential acceptance, occur. However, depending on the architecture and state management approach of your app, you might choose other methods such as:
-
Redux: Instead of emitting events, you could dispatch actions to update the global state. This provides a predictable and centralized way to manage and sync the app's state across different components.
dispatch({
type: "CREDENTIAL_ACCEPTED",
payload: { credentials },
}); -
Context API: For smaller or simpler apps, you might use React's Context API to provide and consume state throughout the component tree.
setContextState((prev) => ({ ...prev, credentials }));
-
React Hooks with State Management Libraries: Libraries like Zustand or Recoil can offer lightweight state management alternatives to Redux.
The choice depends on the complexity of your app, the volume of state updates, and your preference for centralized vs. decentralized state management.
Putting It All Together
- Receiving the credential offer triggers a modal for the user to accept or reject the credentials.
- Acceptance triggers an event that updates the state in the Proof Request screen.
- The event listener in the Proof Request screen automatically updates the acquired credentials.
- The helper function checks if the issued credentials match the items in the proof request.
By following these steps, you ensure a smooth workflow where users can accept credential offers, and the system automatically updates and verifies their acquired credentials. This detailed process covers the automated and manual checks needed to facilitate credential management in your app.